﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using log4net;
using System.Data.Linq.SqlClient;
using System.IO;
using System.Xml;
using RouterControl.Web;
using UPNP.IO;

namespace RouterControl.Db
{
	public sealed class DeviceDomDbConnector
	{
		private readonly ILog _log = LogManager.GetLogger(typeof(DeviceDomDbConnector));

		internal static DeviceDomDbDataContext GetContext()
		{
			return new DeviceDomDbDataContext()
			{
				// for extreme debugging only!!
				//Log = new SqlLogTextWriter()
				//Log = Console.Out
			};
		}

		public void SaveDeviceSchema(DeviceSchema schema)
		{
			if (schema == null)
				throw new ArgumentNullException("schema");
            
			try
			{
				// retrieve device dom
				DeviceDomDb dom = schema.CreateDeviceDom().ToDeviceDomDb();

				// do database stuff
				using (var ctx = GetContext())
				{
					var domDb = (from d in ctx.DeviceDomDbs
								 where SqlMethods.Like(d.SchemaId, schema.SchemaId)
								 select d).Take(1);

					// found?
					if (domDb.Count() <= 0)
					{
						ctx.DeviceDomDbs.InsertOnSubmit(dom);
					}
					else
					{
						var found = domDb.First();

						if (found.RootDevice != null)
							ctx.DeviceDbs.DeleteOnSubmit(found.RootDevice);

						found.RootDevice = dom.RootDevice;
						found.Version = dom.Version;

						dom = found;
					}

					// write schema
					if (dom.SchemaXml == null)
						dom.SchemaXml = new System.Xml.Linq.XDocument();

					using (var xw = dom.SchemaXml.CreateWriter())
					{
						schema.ToXml(xw);
					}

					// update
					ctx.SubmitChanges();
				}
			}
			catch (Exception e)
			{
				_log.Error("Failed to save device schema to DB", e);

				throw;
			}
		}

		public void SaveDeviceDom(DeviceDom dom)
		{
			if (dom == null)
				throw new ArgumentNullException("dom");

			if (string.IsNullOrEmpty(dom.SchemaId))
				throw new ArgumentException("device DOM does not contain a schema id", "dom");

			try
			{
				using (var ctx = GetContext())
				{
					ctx.DeviceDomDbs.InsertOnSubmit(dom.ToDeviceDomDb());

					ctx.SubmitChanges();
				}
			}
			catch (Exception e)
			{
				_log.Error("Failed to save device to DB", e);

				throw;
			}
		}
        /* TODO: implement device dom loading support
		public DeviceDom LoadDeviceDom(string schemaId)
		{
			if (string.IsNullOrEmpty(schemaId))
				throw new ArgumentException("null or empty", "schemaId");

			try
			{
				using (var ctx = GetContext())
				{
					var domDb = (from d in ctx.DeviceDomDbs
								where SqlMethods.Like(d.SchemaId, schemaId)
								select d).Take(1);

					if (domDb.Count() <= 0)
						return null;
	
					return domDb.First().ToDeviceDom();
				}
			}
			catch (Exception e)
			{
				_log.Error("Failed to query device from DB", e);

				throw;
			}
		}*/

		public bool HasDeviceSchema(string schemaId)
		{
			if (string.IsNullOrEmpty(schemaId))
				throw new ArgumentException("null or empty", "schemaId");

			try
			{
				using (var ctx = GetContext())
				{
					var domDb = (from d in ctx.DeviceDomDbs
								 where SqlMethods.Like(d.SchemaId, schemaId)
								 select d.SchemaXml != null).Take(1);

					if (domDb.Count() <= 0)
						return false;	// schema id not found

					return domDb.First();
				}
			}
			catch (Exception e)
			{
				_log.Error("Failed to query device from DB", e);

				throw;
			}
		}

		public bool HasDeviceSchema(string schemaId, int version)
		{
			if (string.IsNullOrEmpty(schemaId))
				throw new ArgumentException("null or empty", "schemaId");

			try
			{
				using (var ctx = GetContext())
				{
					var domDb = (from d in ctx.DeviceDomDbs
								 where d.Version == version && SqlMethods.Like(d.SchemaId, schemaId)
								 select d.SchemaXml != null).Take(1);

					if (domDb.Count() <= 0)
						return false;	// schema id not found

					return domDb.First();
				}
			}
			catch (Exception e)
			{
				_log.Error("Failed to query device from DB", e);

				throw;
			}
		}

		[Obsolete("for Schema DB migration only", true)]
		public void OverwriteDeviceDomCreatedDate(string schemaId, DateTime newDate)
		{
			if (string.IsNullOrEmpty(schemaId))
				throw new ArgumentException("null or empty", "schemaId");

			try
			{
				using (var ctx = GetContext())
				{
					var domDb = (from d in ctx.DeviceDomDbs
								 where d.Created != newDate && SqlMethods.Like(d.SchemaId, schemaId)
								 select d).Take(1);

					if (domDb.Count() <= 0)
						return;	// schema id not found

					// save created date
					var dom = domDb.First();

					dom.Created = newDate;

					ctx.SubmitChanges();
				}
			}
			catch (Exception e)
			{
				_log.Error("Failed to overwrite device DOM created date", e);

				throw;
			}
		}
        /* TODO: implement device dom loading support
		public void ParseAllUnparsedDeviceSchemas()
		{
			ParseAllUnparsedDeviceSchemas(false);
		}

		public void ParseAllUnparsedDeviceSchemas(bool forceParseAll)
		{
			try
			{
				using (var ctx = GetContext())
				{
					// retrieve
					var domDb = (from d in ctx.DeviceDomDbs
								 where d.SchemaXml != null && (forceParseAll || d.RootDevice == null)
								 select d);

					// parse
					var total = domDb.Count();
					var counter = 0;

					_log.Info("Schemas to parse: " + total);

					foreach (var d in domDb)
					{
						try
						{
							// read xml
							var dDoc = new XmlDocument();
	
							using (var xr = d.SchemaXml.CreateReader())
							{
								dDoc.Load(xr);
							}
	
							// parse
							var trans = new DeviceSchemaDomTransform(dDoc);
							
							var dNew = trans.GetDeviceSchemaDom().ToDeviceDomDb();

							// update
							if (d.RootDevice != null)
								ctx.DeviceDbs.DeleteOnSubmit(d.RootDevice);
	
							d.RootDevice = dNew.RootDevice;
							d.MaxSpecParsed = false;
	
							// finalize
							ctx.SubmitChanges();

							_log.InfoFormat(
								"Parsed schema ({0}/{1}): {2}",
								++counter,
								total,
								d.SchemaId
								);
						}
						catch (Exception e)
						{
							_log.Error("Failed to parse schema: " + d.SchemaId, e);
						}
					}
				}
			}
			catch (Exception e)
			{
				_log.Error("Failed to handle unparsed device schemas", e);

				throw;
			}
		}*/

		public void DropDeviceDom(string schemaId)
		{
			if (string.IsNullOrEmpty(schemaId))
				throw new ArgumentException("null or empty", "schemaId");

			try
			{
				using (var ctx = GetContext())
				{
					var domDb = (from d in ctx.DeviceDomDbs
								 where SqlMethods.Like(d.SchemaId, schemaId)
								 select d).Take(1);

					if (domDb.Count() <= 0)
						return;

					ctx.DeviceDomDbs.DeleteOnSubmit(domDb.First());
					ctx.SubmitChanges();
				}
			}
			catch (Exception e)
			{
				_log.Error("Failed to drop device from DB", e);

				throw;
			}
		}

		public int GetUploadedSchemasCount()
		{
			try
			{
				using (var ctx = GetContext())
				{
					ctx.ObjectTrackingEnabled = false;	// read only query

					return (from d in ctx.DeviceDomDbs
							where d.DomType == DeviceDomTypeDb.UserDevice
							select 1).Count();
				}
			}
			catch (Exception e)
			{
				_log.Error("Failed to count device schemas in DB", e);

				throw;
			}
		}

        public struct ReleaseInfo
        {
            public int ReleaseId;
            public string VersionStr;
            public string MsiDownloadUrl;
            public string ReleaseUrl;
        }

        public ReleaseInfo GetLatesRelease(bool publicOnly, bool includeBetaReleases)
        {
            try
            {
                using (var ctx = GetContext())
                {
                    ctx.ObjectTrackingEnabled = false;	// read only query

                    var rel = (from d in ctx.ReleaseDbs
                            where (!publicOnly || d.PublicReleased)
                            && (includeBetaReleases || !d.BetaRelease)
                            orderby d.Id descending
                            select d).Take(1).First();

                    return new ReleaseInfo()
                    {
                        ReleaseId = rel.Id,
                        VersionStr = rel.VersionStr,
                        MsiDownloadUrl = rel.MsiDownloadUrl,
                        ReleaseUrl = rel.ReleaseUrl
                    };
                }
            }
            catch (Exception e)
            {
                _log.Error("Failed to determine latest release", e);

                throw;
            }
        }

		public void LogUpdateCheck(Guid instanceId, int releaseId, int latestReleaseId, string latestVersionStr)
		{
			try
			{                
				using (var ctx = GetContext())
				{
                    if (instanceId == Guid.Empty)
                    {
                        ctx.ExecuteCommand(
                            "insert into UpdateCheckLog (ReleaseId,LastestReleaseId,LatestVersionStr) " +
                            "values ({0},{1},{2})",
                            releaseId,
                            latestReleaseId,
                            latestVersionStr
                            );
                    }
                    else
                    {
                        ctx.ExecuteCommand(
                            "insert into UpdateCheckLog (ReleaseId,LastestReleaseId,LatestVersionStr,InstanceId) " +
                            "values ({0},{1},{2},{3})",
                            releaseId,
                            latestReleaseId,
                            latestVersionStr,
                            instanceId
                            );
                    }
				}
			}
			catch (Exception e)
			{
				_log.Error("Failed to log update check in DB", e);

				throw;
			}
		}

        public class ErrorInfo
        {
            public Guid InstanceId = Guid.Empty;
            public int ReleaseId = -1;
            public DateTime TimeStampUtc;
            public int Count = 1;

            public string ErrorType;
            public string Message;
            public string ClassName;
            public string MethodName;
            public string StackTrace;
            public string ErrorHash;

            public ErrorInfo Inner;

            internal ErrorReportingDb ToDatabase()
            {
                var ret = new ErrorReportingDb()
                {
                    InstanceId = this.InstanceId,
                    ReleaseId = this.ReleaseId,
                    TimeStampUtc = this.TimeStampUtc,
                    Count = this.Count,

                    ErrorType = this.ErrorType,
                    Message = this.Message,
                    ClassName = this.ClassName,
                    MethodName = this.MethodName,
                    StackTrace = this.StackTrace,
                    ErrorHash = this.ErrorHash,
                };

                if (this.Inner != null)
                    ret.Inner.Add(this.Inner.ToDatabase());

                return ret;
            }
        }

        public void AddErrorReport(ErrorInfo ei)
        {
            if (ei == null)
                throw new ArgumentNullException("ei");

            try
            {
                using (var ctx = GetContext())
                {
                    ctx.ErrorReportingDbs.InsertOnSubmit(
                        ei.ToDatabase()
                        );

                    ctx.SubmitChanges();
                }
            }
            catch (Exception e)
            {
                _log.Error("Failed to add error report", e);

                throw;
            }
        }

	    public void UploadUsageInfo(UsageInfo info)
	    {
            if (info == null)
                throw new ArgumentNullException("info");

            try
            {
                using (var ctx = GetContext())
                {
                    ctx.UsageInfoDbs.InsertOnSubmit(
                        info.ToDatabase()
                        );

                    ctx.SubmitChanges();
                }
            }
            catch (Exception e)
            {
                _log.Error("Failed to add usage info", e);

                throw;
            }
        }
	}
}
